Skip to content

[hist] Pause filling during snapshot - #23093

Open
hahnjo wants to merge 1 commit into
root-project:masterfrom
hahnjo:hist-snapshot-pause
Open

[hist] Pause filling during snapshot#23093
hahnjo wants to merge 1 commit into
root-project:masterfrom
hahnjo:hist-snapshot-pause

Conversation

@hahnjo

@hahnjo hahnjo commented Aug 18, 2026

Copy link
Copy Markdown
Member

FillAtomic is much faster than SnapshotAtomic, so in case of heavy contention it would starve the snapshot. By adding a new atomic flag FillAtomic can pause while a Snapshot is running. The successful double collect is still needed if a thread is already in FillAtomic.

If SnapshotAtomic is not used, the performance impact of the added check is minimal / not measurable because fSnapshot is always false and already in the cache line of the RHistEngine.

@hahnjo
hahnjo requested review from hageboeck and jblomer August 18, 2026 11:48
@hahnjo hahnjo self-assigned this Aug 18, 2026
@hahnjo hahnjo added the in:Hist label Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 17h 16m 9s ⏱️
 3 860 tests  3 859 ✅ 0 💤 1 ❌
79 542 runs  79 540 ✅ 1 💤 1 ❌

For more details on these failures, see this check.

Results for commit 313652f.

♻️ This comment has been updated with latest results.

Comment thread hist/histv7/inc/ROOT/RHistEngine.hxx

std::atomic_flag snapshotter;
StressInParallel(NThreads, [&] {
if (!snapshotter.test_and_set()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, this means that the first thread to run will start N consecutive snapshots and lead all the other thread to all do Fills.

Should we also test when a second thread is attempting to do a snapshot (to exercise the while loop on line 873)?

The time taken by the snapshot-ing thread between the end of a snapshot and the start of another one is smallish (2 GetBinContent), is there a risk that with this PR the execution now mostly does all the snapshots before (most of) the Fills? (i.e. not quite but almost executing the whole snapshot-ing threads before the other)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also test when a second thread is attempting to do a snapshot (to exercise the while loop on line 873)?

That's exactly what the "new" test StressSnapshotAtomic does, calling SnapshotAtomic from 4 threads, no? The renamed StressFillSnapshotAtomic then tests the interaction between FillAtomic and SnapshotAtomic, but here we can never have two SnapshotAtomic interacting.

is there a risk that with this PR the execution now mostly does all the snapshots before (most of) the Fills

I don't think so, it also has to destruct the snapshot histogram.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly what the "new" test StressSnapshotAtomic does,

Indeed.

The renamed StressFillSnapshotAtomic ... but here we can never have two SnapshotAtomic interacting.

It 'could' (straightforwardly) be extended to have 2 Snapshot threads hence testing the 3 ways interactions.

I don't think so, it also has to destruct the snapshot histogram.

Fair enough.

FillAtomic is much faster than SnapshotAtomic, so in case of heavy
contention it would starve the snapshot. By adding a new atomic flag
FillAtomic can pause while a Snapshot is running. The successful
double collect is still needed if a thread is already in FillAtomic.

If SnapshotAtomic is not used, the performance impact of the added
check is minimal / not measurable because fSnapshot is always false
and already in the cache line of the RHistEngine.
@hahnjo
hahnjo force-pushed the hist-snapshot-pause branch from a14b914 to 313652f Compare August 19, 2026 12:01
{
ROOT::TestSupport::CheckDiagsRAII diagRAII;
diagRAII.optionalDiag(kWarning, "TKey::TKey", "no public constructor", /*matchFullMessage=*/false);
diagRAII.optionalDiag(kWarning, "TStreamerInfo::Build", "data member \"fSnapshot\" will not be saved",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In which configuration did that appears? (Since fSnapshot is marked transient, we should not be issuing this message)

@pcanal pcanal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks.

std::vector<BinContentType> fBinContents;

/// Flag to pause filling while a snapshot is ongoing
mutable std::atomic<bool> fSnapshot{false}; //!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the comment is a bit late, but this name might better convey the fact that this is set while something is ongoing.

Suggested change
mutable std::atomic<bool> fSnapshot{false}; //!
mutable std::atomic<bool> fSnapshotInProgress{false}; //!

Comment on lines +152 to +153
std::swap(fAxes, rhs.fAxes);
std::swap(fBinContents, rhs.fBinContents);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a snapshot be in progress while this is called? Maybe it's OK to assume that the answer is "no", because moving from something that's in use is likely a problem.

Comment on lines +326 to +336
StressInParallel(NThreads, [&] {
for (std::size_t i = 0; i < NSnapshotsPerThread; i++) {
auto snapshot = engine.SnapshotAtomic();
// compare_exchange wants a non-const reference...
int expected = ExpectedBinContent0;
int actual = snapshot.GetBinContent(0);
if (actual != expected) {
binContent0.compare_exchange_strong(expected, actual);
}
}
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the test below also fills while snapshotting, isn't all what this test does (e.g. to ensure that it doesn't deadlock) also included in the one below?

Comment on lines +873 to +877
do {
while (fSnapshot.load(std::memory_order_relaxed)) {
// Spin while another snapshot is running
}
} while (fSnapshot.exchange(true, std::memory_order_relaxed));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
do {
while (fSnapshot.load(std::memory_order_relaxed)) {
// Spin while another snapshot is running
}
} while (fSnapshot.exchange(true, std::memory_order_relaxed));
// Spin while another snapshot is running and reserve our turn
bool expected = false;
while (! fSnapshot.compare_exchange_strong(expected, true, std::memory_order_relaxed));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants